fix(spec): retire tenancy.organizationField from the authorable surface (#19054) - #19618
Conversation
Strict removal from TenancyConfigSchema + guidance row, the D2/D3 registration, the liveness ledger row, and the platform-internal stamp table that replaces limb 0 in metadata-core. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…lsifies Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
check-issue-citations judged 12 citations this change adds; #8778 and #8707 are allocated-but-absent on the board. The rulings they named are cited in prose and by the cloud record that does resolve. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9f02cde4c5a407d65a9637c05484bdcad66a7880 && git checkout 9f02cde4c5a407d65a9637c05484bdcad66a7880
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4b23e4fab548c43c17754925fe0e1c66dbacd99c 3fb1a4994cb5cfe66f4d67f042213940a9158f4c && git checkout -B drift-repro 4b23e4fab548c43c17754925fe0e1c66dbacd99c && git merge --no-ff 3fb1a4994cb5cfe66f4d67f042213940a9158f4c
node scripts/docs-audit/affected-docs.mjs --json 4b23e4fab548c43c17754925fe0e1c66dbacd99c
|
Contract reviewServed-tier: Reviewed from the diff and the tree, not from the PR narration. Every zero below is paired with the radius it was taken over plus a known target outside it. ① Derived judgmentsAC1 — met. AC2 — met, and it measures the prescription. I traced the channel rather than taking the ablation's word: AC3 — met. The end-to-end half I verified by reading the other two writers' fixtures rather than trusting "untouched": AC4 — met. D2 AC5 — ⭐ NOT met as the card words it. This is the finding. Sanctioned writer #1's pin was edited:
I hold this as a declared, forced finding rather than a block, on three measured grounds: the shape is unreachable for the shipped table ( ② Semver level
③ Boundary flags1. The serial collision is accurately declared, and the file is genuinely generated. #19610's file list does contain 2. The four out-of-surface files — each forced, none bent.
3. The ablations prove what they claim — with one radius named. The second is independently corroborated from source: the refusal pin carries five 4. The unresolvable citations — re-measured, and the PR's claim about them is imprecise. 5. The residue list checks out; one item it does not name. The three sanctioned writers' fixtures still declare the retired key but stay green for the reason given in ① (all keyed 6. CI state at review time (2026-09-21T18:43Z), measured at this head rather than taken from the PR. 39 check runs: 34 Implemented-by: VERDICT: PASS Generated by Claude Code |
只剩一个确认,答「同意」即可落地 — 2026-09-22T16:59Z维护者指示「你的两个 PR 应该跟进到合并」。本 PR 的其余前提已全部清掉,逐条量过:
⇒ 只差 draft 这一步,而翻 draft 是本席位的动作 —— 我不做,只因为下面这一条。 要你确认的那一条(复审自己划出的边界)复审 PASS,但它在 AC5 上明写:
具体是:sanctioned writer #1 的一条对照断言被倒置了 —— ⭐ 但这个形状对已发运的表不可达,我自己在本 PR head 上复核过,⛔ 不是转述复审:
两者都在 ⇒ 那个 所以问题只有一个卡面写的验收判据是「逐字节 / pins 原封不动保持绿」。这条倒置的对照不满足它的字面。你同意按「已记录、已论证、在已发运元数据上不可达」接受它吗?
Generated by Claude Code |
|
| PR | card | hunk | shape |
|---|---|---|---|
| #19657 | #19580 | @@ -5211,7 +5211,35 @@ |
deletes the tail line, re-adds it with a trailing space, appends its own paragraph |
| #19618 | #19054 | @@ -5211,7 +5211,22 @@ |
identical shape, different paragraph |
| #19600 | #15178 | @@ -5211,7 +5218,20 @@ |
identical shape, different paragraph |
All three delete exactly this line:
- + 'selected and no walker can move that intent into the dataset.',
⛔ #19637 is NOT on this region — it edits the same file, but all four of its hunks sit at @@ -9686 and below. Stated so the population is exact rather than 「everything touching the file」.
Second contended point, same class: the conversionIds array immediately below — #19657 at @@ -5232, #19618 and #19600 at @@ -5244.
The rule for whoever lands second and third
- Keep the shared closing line exactly ONCE, and keep the trailing space the first lander added to it.
- Keep every paragraph already on
main, ⛔ not just your own. - Append yours after them.
- Same for
conversionIds: it is consumed as a set, so a dropped id is a retirement that silently stops being declared.
⛔ Why you cannot lean on the usual instruments here
git merge-treeexit 0 is a FALSE GREEN on this file. Measured on this board today: a real merge on PR feat(spec)!: split the translation bundle type —settingsis a platform group, not a per-app one (#15178) #19600 revertedmain'senableOnInstallcorrection in a generated doc whilemerge-treewas happy. Diff the merge commit against BOTH parents, ⛔ not against one.gen:migration-registrywill not save you.registry.ts:18-38says it itself: the generator covers the three<os-generated …>tables, and 「Everything OUTSIDE the markers — this header, each step'srationaleandconversionIds… is still hand-written and still merges as text.」- No gate reds on a dropped paragraph. The result is a syntactically valid string and a green build;
gen:upgrade-guidereprojects from whatever survives, so the only symptom is an upgrade notice that stops mentioning one retirement. ⇒ ⛔ green is not evidence here.
Likely order, so nobody plans against the wrong one
#19600 is closest to landing — contract review PASS on record for its head, CI green on all seven required contexts, mergeable_state: clean. It is held only by its GOVERNED tier H step (skills/objectstack-i18n/SKILL.md), which needs the maintainer's hand or an authorized approval. ⇒ plan on #19600's paragraph being on main first, ⛔ but verify against origin/main at your merge rather than against this sentence.
The structural half is filed, ⛔ not carried here
This tail line reproduces exactly the class #7297 retired for the three tables — the header records it cost 613 hand-resolved lines of conflict markers in four days before that fix. The generator deliberately left rationale outside the markers, and the pattern moved there. not_planned after running the duplicate pass it owed BEFORE filing — the class has been through triage four times (#6957→**#7297** fixed the tables by design; #7464, #8360 and #18062 were each closed, the last folded into #18047, which fixed os-regen-merge.sh's bucketing and ⛔ not this residue). ⛔ An execution seat does not re-litigate a judgement triage has made three times. ⇒ this comment is the record, and ⛔ it changes nothing for the three PRs above, which follow the four rules and land.
Generated by Claude Code
收到
|
更正
|
…ganization-field Base merge only; regeneration follows as its own commit. Hand-resolved (text conflicts, both intents stacked): - packages/spec/src/conversions/registry.ts: CONVERSIONS_BY_MAJOR[18] keeps main's translationPerAppSettingsRemoved and appends this branch's objectTenancyOrganizationFieldRemoved after it (application order within a major = landing order). - packages/spec/src/migrations/registry.ts: step18.rationale keeps the shared closing line once (trailing space), main's translation-bundle paragraph verbatim with its last line re-terminated as a continuation, then this branch's tenancy.organizationField paragraph; step18.conversionIds keeps 'translation-per-app-settings-removed' and adds 'object-tenancy-organization-field-removed'. Driver-deferred (os-regen, regenerated in the next commit): - content/docs/references/system/migration.mdx - content/docs/references/data/object.mdx Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
…dd13) Discharges the os-regen deferral recorded on the merge commit. The driver kept this branch's side of both files; main's side was restored and the pages were regenerated from the merged tree with `pnpm --filter @objectstack/spec build` then `gen:docs`. - content/docs/references/data/object.mdx - content/docs/references/system/migration.mdx Result versus origin/main differs only by this branch's own tenancy.organizationField removal (same changed lines as the branch's pre-merge diff against its merge base). check:generated: all 15 current. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
…at carries it check:future-spec-major (on main since this branch's old base) refuses the prescription's "@objectstack/spec 18": ADR-0087 (amended 2026-09-13) has a tombstone name the npm release it ships in, never the protocol major. This retirement ships as a pre-GA minor, so the carrier is the bare published major, 17. The refusal pin follows the text. Protocol-major references (`os migrate meta --from 17`, `toMajor: 18`, `RETIRED_KEYS_BY_MAJOR[18]`) are unchanged. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
等项目总监契约复审 —— 维护者指示,2026-09-23T06:52Z
出处三件
这意味着什么本 PR 的达档契约复核曾由席位三次尝试起子代理,分别在 2026-09-23T04:15Z、2026-09-23T05:23Z、2026-09-23T06:22Z,三次都因账户本周复核档额度用尽(HTTP 429)在产出任何记录前终止,三次均作废,无可采纳之物。按上述指示,席位不再重试。本 PR 保持 draft、队列外,等项目总监席召唤时做契约复审。 总监复审时可直接用的现状(head
|
Contract reviewServed-tier: Rendered by the director seat (summon #27, ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS — the contract judgments (accept set narrowed as ruled, public surface unwidened, semver and D2 consistent). What remains before Generated by Claude Code |
维护者裁定:倒置对照「同意」—— 2026-09-23T07:21Z
出处三件
裁定的对象
落地前提,逐条对账(head
|
Fixes #19054
Clause-②: no
Executes the maintainer ruling recorded verbatim on the card: 「organizationField 撤出可授权面 同意你的建议」.
object.tenancy.organizationFieldleaves the authorable surface at protocol 18 (ADR-0049 enforce-or-remove). The divergence the key existed for is not retired — only its authorability.What the key was, and why it could never be more than one table's fact
It answered "which column says who this platform row is ABOUT", where
tenancy.tenantFieldanswers "what is this object WALLED by". The spec's own docblock stated the consequence: "For ordinary objects the two coincide andorganizationFieldis never needed." Re-measured at head before this branch: the entire repository declared it once, onpackages/platform-objects/src/identity/sys-api-key.object.ts— the better-auth credential table — and zero business objects declared it. Its readers were three platform-row writers, scope-pinned by name, so an application declaration was inert by construction while still being authorable on every object.The shape of the change
TenancyConfigSchemais astrictObject, so this is the strict-deletion route:TENANCY_RETIRED_KEY_GUIDANCEgains its prescription beside the two v15.0 precedents (tenancy.strategy,tenancy.crossTenantAccess). Authoring it is now refused with the prescription, not strippedobject-tenancy-organization-field-removed(toMajor: 18,retiredFromLoadPath: true) strips it from authored sources and storedsys_metadatarows; D3 wires it into the protocol-18 chain step;RETIRED_KEYS_BY_MAJOR[18]declaresdata/TenancyConfig:organizationFieldauthorable-surface/data.jsonrow is deleted in this same commit — the strict route's tripwire, with the build computing the guidance-route proof for itselfliveness/README.md'sobjectrow records why, andstate-counts.mdmovesobject51 → 50 liveLimb 0 of the shared resolver now reads a platform-internal table instead of a declaration:
keyed by the object's registered NAME, read by the STAMP face alone.
resolveRecordOrganizationFieldandcreateRecordOrganizationResolverkeep their signatures —check:api-surfaceis byte-identical — and the engine-bound face passes the name it was asked about rather than readingobjectDef.name, because several engine doubles in this monorepo return a bare{ tenancy, fields }map with noname.The two facts the card said must survive
sys_api_keyismanagedBy: 'better-auth', soresolveInjectedSystemColumnsbails before tenancy is consulted and noorganization_idis injected. Pinned, and the pin is now stated as the better-auth bail rather than as key-blindness (packages/spec/src/data/injected-system-columns.test.ts).organization_id. In this platform "has anorganization_idcolumn" IS the wall, so the rename would wall the credential table on an equality that excludes NULL.plugin-security's Layer-0 suite pins both halves against the real shipped object.The stamp/wall divergence pin is green:
resolveRecordWallOrganizationFieldnever read the key and is untouched.Base merge after #19600 landed, and the tombstone version it exposed (2026-09-23)
The collision partner this section used to name, #19610, has landed, and so has #19600 (card #15178, merged at 03:04:25Z as
d0f1845657). #19600 is one of the three PRs in the serial onpackages/spec/src/migrations/registry.tsdescribed in notice5780847968. After it landed, this PR readdirty.registry.tsis only partly generated. Its<os-generated …>regions are regenerated. Butregistry.ts:18-38says outright that each step'srationaleandconversionIdsare hand-written and merge as text. No gate turns red when a paragraph is dropped from them.The merge was done on the branch with no rebase and no force-push. It is three commits:
bde765bf05mergesorigin/mainat67add1301a. It is a merge commit with parents7cc0ca1b3dand67add1301a, and it resolved two textual conflicts by hand.step18.rationalekeeps feat(spec)!: split the translation bundle type —settingsis a platform group, not a per-app one (#15178) #19600's paragraph verbatim. Its last line is re-terminated with a trailing space, and this PR's paragraph is appended after it.step18.conversionIdskeeps both'translation-per-app-settings-removed'and'object-tenancy-organization-field-removed'. That gives 33 ids, 33 of them distinct.packages/spec/src/conversions/registry.tskeeps both D2 conversions inCONVERSIONS_BY_MAJOR[18], in landing order. The file's own rule is 「ordering within a major is application order」.9d5fb0ba5fis regeneration only. It regenerates the two reference pages thatos-regen-merge.shhad deferred.3fb1a4994cis a CONTENT change, not merge resolution. The merge brought incheck:future-spec-major(fix(spec,core): ADR-0049 tombstones name the npm release that carries the removal, and a gate keeps them there #19655), which landed after this PR's old base, and CI went red on two sites. Under ADR-0087 (amended 2026-09-13), a tombstone names the npm release it ships in, never the protocol major. This retirement shipsminor, so it lands in 17.x. The commit therefore changes the prescription atpackages/spec/src/data/object.zod.ts:540and its refusal pin atpackages/spec/src/data/object.test.ts:1947from@objectstack/spec 18to@objectstack/spec 17. The protocol-major references (toMajor: 18,RETIRED_KEYS_BY_MAJOR[18],os migrate meta --from 17) are unchanged, because the gate permits them.Measured by the dispatching seat against the committed trees, not taken from the dev's narration:
67add1301a: 2 files, +128/−1. Every hunk is this PR's.7cc0ca1b3d: 2 files, +656/−28. Every hunk is main's.registry.ts, each of the following appears exactly once:dataset. 'settingsis a platform group, not a per-app one (#15178) #19600's paragraphsettingsis a platform group, not a per-app one (#15178) #19600's last line, continuing with a trailing spaceconversionIdssettingsis a platform group, not a per-app one (#15178) #19600's.5765681233) names head7cc0ca1b3d. Commit3fb1a4994cchanges a string that review's AC2 pinned, so this head move is not regeneration-only, and the earlier record does not govern the new head.check-clause2-carriers.mjs --pair 19618confirms it: exit 4, C6. A fresh contract review of the current head is owed before landing.Verification
Re-measured at the current head
3fb1a4994c, after the base merge.CI, measured by the seat from the head's check-runs: 35 checks, latest run per name. 33 success, 2 skipped (
Console Pin Gate,Packed-tarball smoke (opt-in)), 0 failed. All five type-check lanes pass. The legacy commit status issuccess.Suites and gates, from the os-dev report
5788876254, which the seat did not re-run:@objectstack/spec@objectstack/metadata-core@objectstack/plugin-auditcheck:generatedcheck:future-spec-majordispatch-gates --ranmainadded in the merged range.check:future-spec-majorwas checked against a lit control: re-planting18makes it exit 1 with exactly one problem.The tables below are the pre-merge readings, kept as history:
Every number in the tables below was taken at
7cc0ca1b3d, the pre-merge head.Reverse verification (both legs committed first, both restored byte-identically, both via
scripts/ablation-replace.mjs):sys_api_key→sys_api_key_ABLATED)0be02fdcc6b7→21856a4a20d0blob == HEAD,git diff HEADempty2e9e19825ef6→eea8b4c7f061expected 'Unrecognized key(s) on 'tenancy': 'or…' to contain ''tenancy.organizationField' was remov…'— the pin measures the PRESCRIPTION, not merely that parse throws; restore verified the same waySuites (
pnpm testper package, through the shared verify lock):@objectstack/spec@objectstack/metadata-core@objectstack/platform-objects@objectstack/plugin-security@objectstack/plugin-auditTypecheck:
@objectstack/spec,@objectstack/metadata-core,@objectstack/platform-objects,@objectstack/plugin-audit,@objectstack/plugin-security— all green, test layers included.Gates:
node scripts/pm/dispatch-gates.mjs --ranreconciles 114 derived / 114 run / 0 NOT-MEASURED / 0 UNRUN against this diff.pnpm --filter @objectstack/spec check:generatedreports 15 of 15 artifacts current.pnpm lint(eslint . --no-inline-config, the whole repo, no narrowing) exits 0.The three sanctioned platform-row writers' pins stayed green UNTOUCHED, as the card required —
plugin-approvals(approval-node,backfill-platform-row-organizations),service-automation(suspended-run-store),service-storage(backfill-sys-file-organizations): 33 + 52 + 15 tests, zero edits. Thedriver-sqlandtrigger-scheduleread-neutrality suites are green untouched too (36 + 61).Acceptance notes
Declared widening of the dispatched file surface — three files, each because this diff makes a statement in it FALSE. None was edited for tidiness; each is named with the measurement that forced it.
packages/spec/src/shared/alias-integrity.test.ts— RED. It pins the exact key set of the foldedtenancyguidance table:expected [ 'crossTenantAccess', …(2) ] to deeply equal [ 'crossTenantAccess', 'strategy' ]. The retirement adds the third row, which is the only channel the refusal travels on.packages/plugins/plugin-security/src/tenant-layer.test.ts— RED. It asserted the declaration off the shipped object:expected undefined to be 'active_organization_id'. Rewritten to pin what this suite actually owns: the stamp column exists as a field,organization_iddoes not, andtenancyis exactly{ enabled: false }.packages/plugins/plugin-audit/src/audit-writers.test.ts— RED, two cases, and one of them is a finding the card asked for. See the next section.A fourth file,
packages/spec/src/automation/schedule-organization.zod.ts, carried a docblock asserting "tenancy.organizationFieldwins there" — a statement this diff falsifies, and one that publishes, intocontent/docs/references/automation/schedule-organization.mdx. Corrected in prose; the generated page follows.⭐ Finding — one sanctioned writer's pin DID have to be edited, and the reason is not cosmetic. Two
plugin-auditcases went red:organizationFieldoutrankstenantField" pinned the precedence oncrm_lead, an object declaring BOTH keys, with the comment "No shipped object declares both; this pins the precedence so the day one does is not a coin flip." After the retirement no application can declare a stamp column at all, so the question is closed rather than answered. The case is rewritten to pin the closed set — an application object carrying a lookalike column stamps from its own wall.sys_api_keyschema with notenancyblock and pinned the actor's org, proving the stamp came from the declaration rather than from a column-name heuristic. Keying limb 0 by object name makes that shape stampactive_organization_idinstead. This is a real, deliberate behaviour change on a shape that is not reachable for the shipped table —sys_api_keyismanagedBy: 'better-auth'andprotection: { lock: 'full' }, so its block cannot be dropped. Recorded in the rewritten case rather than smoothed over, and the#5315guard that did not move (column absent ⇒ fall through to the actor's org) is pinned beside it.⭐ Finding — two issue citations this repo carries in these files do not resolve.
check-issue-citations --base origin/mainjudged 12 citations this change adds and refused all 12:#8778and#8707areallocated-but-absent(minted, ≤ frontier 19616, not on the board; deleted vs transferred NOT MEASURED). Both are pre-existing text — the diff only re-adds them by rewriting the docblocks around them. Following the gate's own prescription, the added lines now name the rulings in prose and cite the cloud record that does resolve. ⛔ No number was guessed. The standing occurrences on unchanged lines elsewhere in the tree are untouched and are not this PR's to repair.Stale-but-green fixture residue, deliberately NOT touched (green today, outside the dispatched surface, and not a defect — the fixtures feed drivers and engine doubles, never
TenancyConfigSchema):packages/drivers/driver-sql/src/sql-driver-tenant-scope.test.ts,packages/triggers/trigger-schedule/src/time-relative-trigger.test.ts,packages/plugins/plugin-approvals/src/{approval-node,backfill-platform-row-organizations}.test.ts,packages/services/service-automation/src/suspended-run-store.test.ts,packages/services/service-storage/src/backfill-sys-file-organizations.test.tsstill authortenancy: { …, organizationField: … }in raw object-definition fixtures. Their assertions remain true; what has gone vacuous is the claim that the driver / wall face is neutral about a key nobody can write.packages/lint/src/validate-object-field-refs.tscarries the key in a list of scalars it deliberately does not judge.No tree-scoped absence pin is added, and that is a decision rather than an omission: the playbook's tree-scoped form would have to declare its radius in
scripts/cross-package-test-inputs.mjsandturbo.json, both far outside this card's surface, and it would go red against exactly the six inert fixtures above. The absence is instead enforced where it is cheap and exact —authorable-surface/data.jsonhas no row, andcheck:authorable-surfaceis the gate over that baseline.Clause-② re-judged from the diff
no, and the diff agrees. No hunk puts a new key on a published payload: the guidance row is a prescription string, theRETIRED_KEYS_BY_MAJOR/CONVERSIONS_BY_MAJORentries are registry rows,json-schema/**loses a key, andapi-surface/is byte-identical —resolveRecordOrganizationField's signature is unchanged. This is a pure retirement, which narrows.🤖 Generated with Claude Code
https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Generated by Claude Code